﻿using AwesomiumDotNet;
using Mogre;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace MogreSample
{
    class MogreSample : Example
    {
        int Width, Height;
        ColourValue skyColor = new ColourValue(0.6f, 0.7f, 0.8f);
        bool isOverlayMode = false;
        bool isTransparent = false;
        bool areShadowsEnabled = false;
        bool isKeyBoardFocused = false;

        WebCore core;
        WebView view;

        Texture webTexture;
        PanelOverlayElement panel;
        Overlay overlay;
        SceneNode planeNode;
        RaySceneQuery raySceneQuery;
        TerrainCamera terrainCamera;
        MessageHook textInputHandler;

        // For setting the cursor
        const int GCL_HCURSOR = -12;

        [DllImport("user32.dll")]
        public static extern int SetClassLong(IntPtr hWnd, int nIndex, long dwNewLong);


        public override void CreateCamera()
        {
            camera = sceneMgr.CreateCamera("mainCamera");
            SceneNode camNode = sceneMgr.RootSceneNode.CreateChildSceneNode("camNode");
            terrainCamera = new TerrainCamera(camNode, camera);

            camera.FarClipDistance = 950;
            camera.NearClipDistance = 1;
        }

        public override void CreateViewports()
        {
            viewport = window.AddViewport(camera);
            viewport.BackgroundColour = skyColor;

            if (root.RenderSystem.Capabilities.NonPOW2TexturesLimited)
            {
                Width = Height = 512;
            }
            else
            {
                Width = viewport.ActualWidth;
                Height = viewport.ActualHeight;
            }
        }

        public override void CreateScene()
        {
            ResourceGroupManager.Singleton.AddResourceLocation("./html", "FileSystem", "General");
            sceneMgr.SetWorldGeometry("terrain.cfg");
            terrainCamera.ClampToTerrain();


            // Set ambient light and fog
            sceneMgr.AmbientLight = new ColourValue(1.0f, 0.0f, 0.0f, 0);
            sceneMgr.SetFog(FogMode.FOG_LINEAR, skyColor, 0, 400, 1000);

            // Create sun-light
            Light light = sceneMgr.CreateLight("Sun");
            light.Type = Light.LightTypes.LT_DIRECTIONAL;
            light.Direction = new Vector3(0, -1, -0.8f);

            sceneMgr.ShadowTechnique = ShadowTechnique.SHADOWTYPE_NONE;
		    sceneMgr.ShadowFarDistance = 260;
		    sceneMgr.ShadowColour = new ColourValue(0.7f, 0.7f, 0.7f);
		    sceneMgr.SetShadowTextureSize((ushort)(Width > 800 ? 1024 : 512));


            CreateOverlayAndMaterials();
            SetupAwesomium();


            Plane plane = new Plane(Vector3.UNIT_X, 0);
            MeshManager.Singleton.CreatePlane("webPlane", ResourceGroupManager.DEFAULT_RESOURCE_GROUP_NAME,
                plane, Width, Height, 2, 2, true, 1, 1, 1, Vector3.UNIT_Y);

            Entity planeEnt = sceneMgr.CreateEntity("webPlaneEntity", "webPlane");
            planeEnt.SetMaterialName("webPlaneMaterial");
            planeEnt.CastShadows = true;

            planeNode = sceneMgr.RootSceneNode.CreateChildSceneNode();
            planeNode.AttachObject(planeEnt);
            planeNode.Scale(2.5f, 0.25f, 0.25f);

            SetOverlayMode(false);

            raySceneQuery = sceneMgr.CreateRayQuery(new Ray());
        }

        void SetupAwesomium()
        {
            WebCoreOptions options = new WebCoreOptions();
            options.EnablePlugins = true;
            options.LogLevel = LogLevel.None;
            core = new WebCore(options);
            core.BaseDirectory = "html\\";
            //core.SetCustomResponsePage(404, "404response.html");

            view = core.CreateWebView(Width, Height, isTransparent);
            view.Callback += new EventHandler<CallbackEventArgs>(view_Callback);
            view.ChangeCursor += new EventHandler<ChangeCursorEventArgs>(view_ChangeCursor);
            view.ChangeKeyboardFocus += new EventHandler<ChangeKeyboardFocusEventArgs>(view_ChangeKeyboardFocus);
            view.CreateObject("Client");
            view.SetObjectProperty("Client", "welcomeMsg", new JSValue("Hello World!"));
            view.SetObjectProperty("Client", "renderSystem", new JSValue(Root.Singleton.RenderSystem.Name));
            view.SetObjectCallback("Client", "requestFPS");

            view.LoadFile("demo.html");
        }

        void view_ChangeKeyboardFocus(object sender, ChangeKeyboardFocusEventArgs e)
        {
            isKeyBoardFocused = e.IsFocused;
        }

        void view_ChangeCursor(object sender, ChangeCursorEventArgs e)
        {
            IntPtr windowHnd;
            window.GetCustomAttribute("WINDOW", out windowHnd);
            SetClassLong(windowHnd, GCL_HCURSOR, e.Cursor.Handle.ToInt32());
        }

        void view_Callback(object sender, CallbackEventArgs e)
        {
            if(e.CallbackName == "requestFPS")
		    {
			    RenderTarget.FrameStats stats = viewport.Target.GetStatistics();
		    	view.SetObjectProperty("Client", "fps", new JSValue((int)stats.LastFPS));
	    		view.ExecuteJavaScript("updateFPS()");
    		}
        }

        void CreateOverlayAndMaterials()
        {
            string texName = "webTexture";
            string materialName = "webMaterial";
            string planeMaterialName = "webPlaneMaterial";

            webTexture = TextureManager.Singleton.CreateManual(
                texName, ResourceGroupManager.DEFAULT_RESOURCE_GROUP_NAME,
                TextureType.TEX_TYPE_2D, (uint)Width, (uint)Height, 0,
                Mogre.PixelFormat.PF_BYTE_BGRA,
                (int)TextureUsage.TU_DYNAMIC_WRITE_ONLY_DISCARDABLE);


            // Create Overlay Material
            MaterialPtr material = MaterialManager.Singleton.Create(
                materialName, ResourceGroupManager.DEFAULT_RESOURCE_GROUP_NAME);

            Pass pass = material.GetTechnique(0).GetPass(0);
            pass.DepthCheckEnabled = false;
            pass.DepthWriteEnabled = false;
            pass.LightingEnabled = false;
            pass.SetSceneBlending(SceneBlendType.SBT_REPLACE);

            TextureUnitState texUnit = pass.CreateTextureUnitState(texName);
            texUnit.SetTextureFiltering(FilterOptions.FO_NONE,
                FilterOptions.FO_NONE, FilterOptions.FO_NONE);


            // Create Plane Material

            material = MaterialManager.Singleton.Create(planeMaterialName,
                ResourceGroupManager.DEFAULT_RESOURCE_GROUP_NAME);

            pass = material.GetTechnique(0).GetPass(0);
            pass.LightingEnabled = false;
            pass.SetSceneBlending(SceneBlendType.SBT_REPLACE);
            pass.CullingMode = CullingMode.CULL_NONE;


            texUnit = pass.CreateTextureUnitState(texName);
            texUnit.SetTextureFiltering(TextureFilterOptions.TFO_ANISOTROPIC);
            texUnit.TextureAnisotropy = 4;


            OverlayManager overlayManager = OverlayManager.Singleton;

            panel = (PanelOverlayElement)overlayManager.CreateOverlayElement("Panel", "webPanel");
            panel.MetricsMode = GuiMetricsMode.GMM_PIXELS;
            panel.MaterialName = materialName;
            panel.SetDimensions(Width, Height);
            panel.SetPosition(0, 0);

            overlay = overlayManager.Create("webOverlay");
            overlay.Add2D(panel);
            overlay.Show();
        }

        /*
        void ResizeOverlay(int width, int height)
	    {
		    Width = width;
		    Height = height;

		    MaterialPtr mat = MaterialManager.Singleton.GetByName("webMaterial");
		    Pass pass = mat.GetTechnique(0).GetPass(0);
		    pass.RemoveAllTextureUnitStates();

		    MaterialManager.Singleton.Remove("webTexture");
		    webTexture.Dispose();

            webTexture = TextureManager.Singleton.CreateManual(
                "webTexture", ResourceGroupManager.DEFAULT_RESOURCE_GROUP_NAME,
                TextureType.TEX_TYPE_2D, (uint)Width, (uint)Height, 0,
                Mogre.PixelFormat.PF_BYTE_BGRA,
                (int)TextureUsage.TU_DYNAMIC_WRITE_ONLY_DISCARDABLE);

		    TextureUnitState texUnit = pass.CreateTextureUnitState("webTexture");
		    texUnit.SetTextureFiltering(FilterOptions.FO_NONE, FilterOptions.FO_NONE, FilterOptions.FO_NONE);

		    panel.SetDimensions(Width, Height);
	    }
        */
        
        public override void DestroyScene()
        {
            base.DestroyScene();

            view.Dispose();
            core.Dispose();

            textInputHandler.MessageReceived -= textInputHandler_MessageReceived;
            if (textInputHandler != null)
                textInputHandler.Dispose();

            if (raySceneQuery != null)
                raySceneQuery.Dispose();

            if (sceneMgr != null)
                sceneMgr.ClearScene();

            webTexture.Dispose();

            if (Root.Singleton != null)
                Root.Singleton.Dispose();
        }

        public override void CreateFrameListener()
        {
            base.CreateFrameListener();
            root.FrameStarted += new FrameListener.FrameStartedHandler(root_FrameStarted);
        }

       public override void CreateInput()
        {
            base.CreateInput();

            IntPtr windowHnd;
            window.GetCustomAttribute("WINDOW", out windowHnd);
            textInputHandler = new MessageHook(windowHnd);
            textInputHandler.MessageReceived += new EventHandler(textInputHandler_MessageReceived);

            inputKeyboard.KeyPressed += new MOIS.KeyListener.KeyPressedHandler(inputKeyboard_KeyPressed);
            inputMouse.MouseMoved += new MOIS.MouseListener.MouseMovedHandler(inputMouse_MouseMoved);
            inputMouse.MousePressed += new MOIS.MouseListener.MousePressedHandler(inputMouse_MousePressed);
            inputMouse.MouseReleased += new MOIS.MouseListener.MouseReleasedHandler(inputMouse_MouseReleased);

            MOIS.MouseState_NativePtr mouseState = inputMouse.MouseState;
            mouseState.width = viewport.ActualWidth;
            mouseState.height = viewport.ActualHeight;
        }

        void textInputHandler_MessageReceived(object sender, EventArgs e)
        {
            if (isKeyBoardFocused)
                view.InjectKeyboardEvent(textInputHandler.LastMessage);
        }

        bool inputKeyboard_KeyPressed(MOIS.KeyEvent e)
        {
            switch (e.key)
            {
                case MOIS.KeyCode.KC_F1:
                    SetOverlayMode(!isOverlayMode);
                    break;
                case MOIS.KeyCode.KC_F2:
                    SetTransparent(!isTransparent);
                    break;
                case MOIS.KeyCode.KC_F3:
                    view.LoadFile("demo.html");
                    break;
                case MOIS.KeyCode.KC_F4:
                    view.ZoomIn();
                    break;
                case MOIS.KeyCode.KC_F5:
                    view.ZoomOut();
                    break;
                case MOIS.KeyCode.KC_F6:
                    switch (camera.PolygonMode)
                    {
                        case PolygonMode.PM_POINTS:
                            camera.PolygonMode = PolygonMode.PM_SOLID;
                            break;
                        case PolygonMode.PM_SOLID:
                            camera.PolygonMode = PolygonMode.PM_WIREFRAME;
                            break;
                        case PolygonMode.PM_WIREFRAME:
                            camera.PolygonMode = PolygonMode.PM_POINTS;
                            break;
                    }
                    break;
                case MOIS.KeyCode.KC_F7:
        			areShadowsEnabled = !areShadowsEnabled;
			        if(areShadowsEnabled)
                        sceneMgr.ShadowTechnique = ShadowTechnique.SHADOWTYPE_TEXTURE_MODULATIVE;
    	    		else
                        sceneMgr.ShadowTechnique = ShadowTechnique.SHADOWTYPE_NONE;
                    break;
                case MOIS.KeyCode.KC_F8:
                    sceneMgr.ShowBoundingBoxes = !sceneMgr.ShowBoundingBoxes;
                    break;
                case MOIS.KeyCode.KC_SYSRQ:
                    TakeScreenshot();
                    break;
            }
            return true;
        }

        protected override void HandleInput(FrameEvent evt)
        {
            bool isTranslating = false;
            Vector3 translation = Vector3.ZERO;
            float delta = evt.timeSinceLastFrame * 100;

            inputKeyboard.Capture();
            inputMouse.Capture();

            if (inputKeyboard.IsKeyDown(MOIS.KeyCode.KC_ESCAPE))
            {
                // stop rendering loop
                shutDown = true;
            }

            if (isKeyBoardFocused)
                return;

            if (inputKeyboard.IsKeyDown(MOIS.KeyCode.KC_W))
            {
                translation += new Vector3(0, 0, delta);
                isTranslating = true;
            }

            if (inputKeyboard.IsKeyDown(MOIS.KeyCode.KC_S))
            {
                translation += new Vector3(0, 0, -delta);
                isTranslating = true;
            }

            if (inputKeyboard.IsKeyDown(MOIS.KeyCode.KC_A))
            {
                translation += new Vector3(delta, 0, 0);
                isTranslating = true;
            }

            if (inputKeyboard.IsKeyDown(MOIS.KeyCode.KC_D))
            {
                translation += new Vector3(-delta, 0, 0);
                isTranslating = true;
            }

            if (isTranslating)
            {
                terrainCamera.Translate(translation);
                terrainCamera.ClampToTerrain();
            }
        }

        bool inputMouse_MouseMoved(MOIS.MouseEvent arg)
        {
            if (arg.state.ButtonDown(MOIS.MouseButtonID.MB_Right))
            {
                // If we are in camera-pivote state, spin/pitch the camera based
                // on relative mouse movement.
                terrainCamera.Spin(new Degree(-arg.state.X.rel * 0.14f));
                terrainCamera.Pitch(new Degree(-arg.state.Y.rel * 0.1f));
                return true;
            }

            if (arg.state.Z.rel != 0)
                view.InjectMouseWheel(arg.state.Z.rel / 2);
            else
            {
                if (isOverlayMode)
                {
                    view.InjectMouseMove(arg.state.X.abs, arg.state.Y.abs);
                }
                else
                {
                    int localX, localY;

                    if (webPlaneHitTest(arg.state.X.abs, arg.state.Y.abs, arg.state.width, arg.state.height, out localX, out localY))
                    {
                        view.InjectMouseMove(localX, localY);
                    }
                }
            }

            return true;
        }

        bool inputMouse_MousePressed(MOIS.MouseEvent arg, MOIS.MouseButtonID id)
        {
            if (id == MOIS.MouseButtonID.MB_Left)
            {
                if (isOverlayMode)
                {
                    view.InjectMouseDown(MouseButton.Left);
                }
                else
                {
                    int localX, localY;

                    if (webPlaneHitTest(arg.state.X.abs, arg.state.Y.abs, arg.state.width, arg.state.height, out localX, out localY))
                    {
                        view.InjectMouseDown(MouseButton.Left);
                        view.Focus();
                    }
                }
            }
            else if (id == MOIS.MouseButtonID.MB_Right)
            {
                view.Unfocus();
                isKeyBoardFocused = false;
            }

            return true;
        }

        bool inputMouse_MouseReleased(MOIS.MouseEvent arg, MOIS.MouseButtonID id)
        {
            if (id == MOIS.MouseButtonID.MB_Left)
                view.InjectMouseUp(MouseButton.Left);

            return true;
        }

        bool root_FrameStarted(FrameEvent evt)
        {
            Draw();
            core.Update();
            return true;
        }

        public void Draw()
        {
            if (!view.IsDirty())
                return;

            HardwarePixelBufferSharedPtr pixelBuffer = webTexture.GetBuffer();

            unsafe
            {
                pixelBuffer.Lock(HardwareBuffer.LockOptions.HBL_DISCARD);
            }
            PixelBox pixelBox = pixelBuffer.CurrentLock;
            uint dstBpp = PixelUtil.GetNumElemBytes(pixelBox.format);
            uint dstPitch = pixelBox.rowPitch * dstBpp;

            view.Render(pixelBox.data, (int)dstPitch, (int)dstBpp);

            pixelBuffer.Unlock();
        }

        void SetOverlayMode(bool isOverlayMode)
        {
            view.Unfocus();

            if (isOverlayMode)
            {
                overlay.Show();
                planeNode.SetVisible(false);
                sceneMgr.RootSceneNode.SetVisible(false);
                this.isOverlayMode = true;
                view.Focus();
            }
            else
            {
                overlay.Hide();
                terrainCamera.OrientPlaneToCamera(planeNode, Height / 4);
                planeNode.SetVisible(true);
                sceneMgr.RootSceneNode.SetVisible(true);
                this.isOverlayMode = false;
            }
        }

        void SetTransparent(bool isTransparent)
        {
            view.SetTransparent(isTransparent);

            Pass overlayMatPass = ((MaterialPtr)MaterialManager.Singleton.
                GetByName("webMaterial")).GetTechnique(0).GetPass(0);
            Pass planeMatPass = ((MaterialPtr)MaterialManager.Singleton.
                GetByName("webPlaneMaterial")).GetTechnique(0).GetPass(0);

            if (isTransparent)
            {
                overlayMatPass.SetSceneBlending(SceneBlendType.SBT_TRANSPARENT_ALPHA);
                planeMatPass.SetSceneBlending(SceneBlendType.SBT_TRANSPARENT_ALPHA);
                this.isTransparent = true;
            }
            else
            {
                overlayMatPass.SetSceneBlending(SceneBlendType.SBT_REPLACE);
                planeMatPass.SetSceneBlending(SceneBlendType.SBT_REPLACE);
                this.isTransparent = false;
            }
        }

        // Based on code from the 'BrowserPlaneDemo'
	    bool webPlaneHitTest(int x, int y, int width, int height, out int outX, out int outY)
	    {
            outX = 0;
            outY = 0;
		    bool hit = false;

		    Ray mouseRay = camera.GetCameraToViewportRay(x / width, y / height);
            
		    raySceneQuery.Ray = mouseRay;
		    raySceneQuery.SetSortByDistance(true);

		    RaySceneQueryResult result = raySceneQuery.Execute();
            
		    for(RaySceneQueryResult.Iterator i = result.Begin(); i != result.End(); i++)
			    if(i.Value.movable != null && i.Value.movable.Name.Equals("webPlaneEntity"))
				     hit = rayHitPlane((Entity)i.Value.movable, mouseRay, out outX, out outY);

		    raySceneQuery.ClearResults();
            
		    return hit;
	    }

        // Get the mesh information for the given mesh.
        public unsafe void GetMeshInformation(MeshPtr mesh,
            ref uint vertex_count,
            ref Vector3[] vertices,
            ref uint index_count,
            ref UInt32[] indices,
            Vector3 position,
            Quaternion orientation,
            Vector3 scale)
        {
            bool added_shared = false;
            uint current_offset = 0;
            uint shared_offset = 0;
            uint next_offset = 0;
            uint index_offset = 0;

            vertex_count = index_count = 0;

            for (ushort i = 0; i < mesh.NumSubMeshes; ++i)
            {
                SubMesh submesh = mesh.GetSubMesh(i);
                if (submesh.useSharedVertices)
                {
                    if (!added_shared)
                    {
                        vertex_count += mesh.sharedVertexData.vertexCount;
                        added_shared = true;
                    }
                }
                else
                {
                    vertex_count += submesh.vertexData.vertexCount;
                }

                index_count += submesh.indexData.indexCount;
            }

            vertices = new Vector3[vertex_count];
            indices = new UInt32[index_count];
            added_shared = false;

            for (ushort i = 0; i < mesh.NumSubMeshes; ++i)
            {
                SubMesh submesh = mesh.GetSubMesh(i);
                VertexData vertex_data = submesh.useSharedVertices ? mesh.sharedVertexData : submesh.vertexData;

                if (!submesh.useSharedVertices || (submesh.useSharedVertices && !added_shared))
                {
                    if (submesh.useSharedVertices)
                    {
                        added_shared = true;
                        shared_offset = current_offset;
                    }

                    VertexElement posElem =
                        vertex_data.vertexDeclaration.FindElementBySemantic(VertexElementSemantic.VES_POSITION);
                    HardwareVertexBufferSharedPtr vbuf =
                        vertex_data.vertexBufferBinding.GetBuffer(posElem.Source);

                    byte* vertex = (byte*)vbuf.Lock(HardwareBuffer.LockOptions.HBL_READ_ONLY);
                    float* pReal;

                    for (int j = 0; j < vertex_data.vertexCount; ++j, vertex += vbuf.VertexSize)
                    {
                        posElem.BaseVertexPointerToElement(vertex, &pReal);
                        Vector3 pt = new Vector3(pReal[0], pReal[1], pReal[2]);
                        vertices[current_offset + j] = (orientation * (pt * scale)) + position;
                    }
                    vbuf.Unlock();
                    next_offset += vertex_data.vertexCount;
                }

                IndexData index_data = submesh.indexData;
                uint numTris = index_data.indexCount / 3;
                HardwareIndexBufferSharedPtr ibuf = index_data.indexBuffer;

                bool use32bitindexes = (ibuf.Type == HardwareIndexBuffer.IndexType.IT_32BIT);

                ulong* pLong = (ulong*)ibuf.Lock(HardwareBuffer.LockOptions.HBL_READ_ONLY);
                ushort* pShort = (ushort*)pLong;
                uint offset = submesh.useSharedVertices ? shared_offset : current_offset;
                if (use32bitindexes)
                {
                    for (int k = 0; k < index_data.indexCount; ++k)
                    {
                        indices[index_offset++] = (UInt32)pLong[k] + (UInt32)offset;
                    }
                }
                else
                {
                    for (int k = 0; k < index_data.indexCount; ++k)
                    {
                        indices[index_offset++] = (UInt32)pShort[k] + (UInt32)offset;
                    }
                }
                ibuf.Unlock();
                current_offset = next_offset;
            }

        }

        // Based on code from the 'BrowserPlaneDemo'
        bool rayHitPlane(Entity plane, Ray ray, out int outX, out int outY)
        {
            outX = 0;
            outY = 0;
		    float closest_distance = -1.0f;
      
		    uint vertexCount=0, indexCount=0;
		    Vector3[] vertices=null;
		    UInt32[] indices=null;

		    /**
		    * Note: Since this is just a demo, care hasn't been taken to optimize everything
		    *		to the extreme, however, for a production setting, it would be a bit
		    *		more efficient to save the mesh information for each Entity we care
		    *		about so that we aren't needlessly calling the following function every
		    *		time we wish to a little mouse-picking.
		    */

		    // get the mesh information
		    GetMeshInformation(plane.GetMesh(), ref vertexCount, ref vertices, ref indexCount, ref indices,             
			    plane.ParentNode._getDerivedPosition(), plane.ParentNode._getDerivedOrientation(),
			    plane.ParentNode.GetScale());

		    // test for hitting individual triangles on the mesh
		    bool new_closest_found = false;
            Mogre.Pair<bool, float> intersectTest;
		    for(int i = 0; i < indexCount; i += 3)
		    {
			    /**
			    * Note: We could also check for hits on the back-side of the plane by swapping
			    *		the 'true' and 'false' tokens in the below statement.
			    */

			    // check for a hit against this triangle
			    intersectTest = Mogre.Math.Intersects(ray, vertices[indices[i]], vertices[indices[i+1]], 
			    vertices[indices[i+2]], true, false);

			    // if it was a hit check if its the closest
			    if(intersectTest.first)
			    {
				    if((closest_distance < 0.0f) || (intersectTest.second < closest_distance))
				    {
					    // this is the closest so far, save it off
					    closest_distance = intersectTest.second;
					    new_closest_found = true;
				    }
			    }
		    }

		    // get the parent node
		    SceneNode browserNode = plane.ParentSceneNode;

		    // if we found a new closest raycast for this object, update the
		    // closest_result before moving on to the next object.
		    if(browserNode != null && new_closest_found)
		    {
			    Vector3 pointOnPlane = ray.GetPoint(closest_distance);
			    Quaternion quat = browserNode._getDerivedOrientation().Inverse();
			    Vector3 result = quat * pointOnPlane;
			    Vector3 positionInWorld = quat * browserNode._getDerivedPosition();
			    Vector3 scale = browserNode._getDerivedScale();

                outX = (Width / 2) - (int)((result.z - positionInWorld.z) / scale.z);
                outY = (Height / 2) - (int)((result.y - positionInWorld.y) / scale.y);

                return true;
		    }

		    return false;
        }
    }
}
